/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ANONYMOUS_METHOD_PREFIX, ArkMethod, DEFAULT_ARK_CLASS_NAME, FunctionType, Local, Stmt, Value } from 'arkanalyzer';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { CheckerUtils, Defects, MatcherCallback, MatcherTypes, MethodMatcher, Rule } from '../../Index';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { IssueReport } from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'AvoidAnimationInLifecycle');
const SIGNZTURES: string[] = [
    `@ohosSdk/component/common.d.ts: ${DEFAULT_ARK_CLASS_NAME}.animateTo(@ohosSdk/component/common.d.ts: AnimateParam, @ohosSdk/component/common.d.ts: ${DEFAULT_ARK_CLASS_NAME}.${ANONYMOUS_METHOD_PREFIX}0())`,
    `@ohosSdk/component/common.d.ts: ${DEFAULT_ARK_CLASS_NAME}.animateToImmediately(@ohosSdk/component/common.d.ts: AnimateParam, @ohosSdk/component/common.d.ts: ${DEFAULT_ARK_CLASS_NAME}.${ANONYMOUS_METHOD_PREFIX}1())`,
    `@ohosSdk/api/@ohos.arkui.UIContext.d.ts: UIContext.animateTo(@ohosSdk/component/common.d.ts: AnimateParam, @ohosSdk/api/@ohos.arkui.UIContext.d.ts: UIContext.%AM0())`,
    `@ohosSdk/api/@ohos.arkui.UIContext.d.ts: UIContext.keyframeAnimateTo(@ohosSdk/component/common.d.ts: KeyframeAnimateParam, @ohosSdk/component/common.d.ts: KeyframeState[])`
];
const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/avoid-animation-in-lifecycle.md',
    description: 'Avoid calling animation interfaces (animateTo, animateToImmediately, keyframeAnimateTo) in aboutToAppear/aboutToDisappear lifecycle callbacks.'
};

export class AvoidAnimationInLifecycle implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public issues: IssueReport[] = [];

    private mtdMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
        name: ['aboutToAppear', 'aboutToDisappear'],
    };

    public registerMatchers(): MatcherCallback[] {
        const matchMethodCb: MatcherCallback = {
            matcher: this.mtdMatcher,
            callback: this.check
        };
        return [matchMethodCb];
    }

    public check = (target: ArkMethod): void => {
        // 使用Set来记录已检查的方法签名，避免重复检查
        const checkedMethods = new Set<string>();
        // 使用队列来存储待检查的方法，避免递归
        const methodsToCheck = [target];
        while (methodsToCheck.length > 0) {
            const currentMethod = methodsToCheck.shift()!;
            const methodSignature = currentMethod.getSignature().toString();
            // 如果该方法已检查过，则跳过
            if (checkedMethods.has(methodSignature)) {
                continue;
            }
            // 标记该方法为已检查
            checkedMethods.add(methodSignature);
            const stmts = currentMethod.getBody()?.getCfg().getStmts() ?? [];
            this.processStmts(currentMethod, stmts, methodsToCheck);
        }
    };

    // 处理方法语句
    private processStmts(currentMethod: ArkMethod, stmts: Stmt[], methodsToCheck: ArkMethod[]): void {
        for (let stmt of stmts) {
            const invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokeExpr) {
                continue;
            }
            const methodSignature = invokeExpr.getMethodSignature();
            const methodSignatureStr = methodSignature.toString();
            const name = methodSignature.getMethodSubSignature().getMethodName();
            if (SIGNZTURES.includes(methodSignatureStr)) {
                this.reportIssue(stmt, name);
            } else {
                // 处理方法参数中的函数类型
                const args = invokeExpr.getArgs();
                if (args.length !== 0) {
                    const nestedMethods = this.extractMethodsFromArgs(currentMethod, args);
                    methodsToCheck.push(...nestedMethods);
                }
                // 处理被调用的方法
                const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile();
                const declaringMethod = arkFile.getScene().getMethod(methodSignature);
                if (declaringMethod && !declaringMethod.isGenerated()) {
                    methodsToCheck.push(declaringMethod);
                }
            }
        }
    }

    // 从方法参数中提取函数类型对应的方法
    private extractMethodsFromArgs(mtd: ArkMethod, args: Value[]): ArkMethod[] {
        const methods: ArkMethod[] = [];
        for (let arg of args) {
            if (!(arg instanceof Local)) {
                continue;
            }
            const type = arg.getType();
            if (!(type instanceof FunctionType)) {
                continue;
            }
            const method = mtd.getDeclaringArkFile().getScene().getMethod(type.getMethodSignature());
            if (!method) {
                continue;
            }
            methods.push(method);
        }
        return methods;
    }

    private reportIssue(stmt: Stmt, methodName: string): void {
        const text = stmt.getOriginalText();
        if (!text || text.length === 0) {
            logger.debug('Stmt text is empty.');
            return;
        }
        const index = text.indexOf(methodName);
        if (index === -1) {
            return;
        }
        const severity = this.rule.alert ?? this.metaData.severity;
        const originalPosition = stmt.getOriginPositionInfo();
        const lineNum = originalPosition.getLineNo();
        const startColum = originalPosition.getColNo() + index;
        const endColum = startColum + methodName.length - 1;
        const filePath = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile().getFilePath();
        let defects = new Defects(lineNum, startColum, endColum, this.metaData.description, severity, this.rule.ruleId, filePath,
            this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}